﻿//==============================================================================
// File: NumericSpinner.cs
// Created: 2010-05-28
// Author: Piotr Włodek, Arek Świerczek
//==============================================================================
// This file is a part of MAMMOTH project.
// Copyright (C) 2010 AGH University of Science and Technology, Krakow.
// https://caribou.iisg.agh.edu.pl/trac/mammoth
//
// Mammoth is free software: you can redistribute it and/or modify
// it under the terms of the GNU Lesser General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
// 
// Mammoth is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
// GNU Lesser General Public License for more details.
// 
// You should have received a copy of the GNU Lesser General Public License
// along with Mammoth. If not, see http://www.gnu.org/licenses/.
//==============================================================================

using System;
using System.Globalization;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Controls.Primitives;
using System.Windows.Data;
using System.Windows.Input;         // RoutedCommand
using System.Windows.Threading;     // Dispatcher

namespace WpfRcp.Controls
{
    /// <summary>
    /// Represents a up-down control that displays numeric values
    /// </summary>
    public class NumericSpinner : Control
    {
        #region Constructors

        /// <summary>
        /// Static Constructor
        /// </summary>
        static NumericSpinner()
        {
            DefaultStyleKeyProperty.OverrideMetadata(typeof(NumericSpinner), new FrameworkPropertyMetadata(typeof(NumericSpinner)));

            _decreaseCommand = new RoutedCommand("DecreaseCommand", typeof(NumericSpinner));
            _increaseCommand = new RoutedCommand("IncreaseCommand", typeof(NumericSpinner));
            CommandManager.RegisterClassCommandBinding(typeof(NumericSpinner), new CommandBinding(DecreaseCommand, OnExecuteDecreaseCommand, OnQueryDecreaseCommand));
            CommandManager.RegisterClassCommandBinding(typeof(NumericSpinner), new CommandBinding(IncreaseCommand, OnExecuteIncreaseCommand, OnQueryIncreaseCommand));

            IsTabStopProperty.OverrideMetadata(typeof(NumericSpinner), new FrameworkPropertyMetadata(false));
        }

        #endregion

        #region Public Methods

        public override void OnApplyTemplate()
        {
            base.OnApplyTemplate();
            AttachToVisualTree();
        }

        public override string ToString()
        {
            return base.ToString() + " Maximum:" + Maximum + " Minimum:" + Minimum + " Value:" + Value;
        }

        #endregion

        #region Public Properties

        #region Maximum/Minimum/Value

        /// <summary>
        ///     The DependencyProperty for the Minimum property.
        ///     Flags:              none
        ///     Default Value:      0
        /// </summary>
        public static readonly DependencyProperty MinimumProperty =
            DependencyProperty.Register(
                "Minimum",
                typeof(decimal),
                typeof(NumericSpinner),
                new FrameworkPropertyMetadata(
                    decimal.MinValue,
                    new PropertyChangedCallback(OnMinimumChanged)));

        /// <summary>
        ///     Minimum restricts the minimum value of the Value property
        /// </summary>
        public decimal Minimum
        {
            get { return (decimal)GetValue(MinimumProperty); }
            set { SetValue(MinimumProperty, value); }
        }

        /// <summary>
        ///     Called when MinimumProperty is changed on "d."
        /// </summary>
        private static void OnMinimumChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
        {
            var ctrl = (NumericSpinner)d;

            ctrl.CoerceValue(MaximumProperty);
            ctrl.CoerceValue(ValueProperty);
        }

        /// <summary>
        ///     The DependencyProperty for the Maximum property.
        ///     Flags:              none
        ///     Default Value:      1
        /// </summary>
        public static readonly DependencyProperty MaximumProperty =
            DependencyProperty.Register(
                "Maximum",
                typeof(decimal),
                typeof(NumericSpinner),
                new FrameworkPropertyMetadata(
                    decimal.MaxValue,
                    new PropertyChangedCallback(OnMaximumChanged),
                    CoerceMaximum));

        private static object CoerceMaximum(DependencyObject d, object value)
        {
            var ctrl = (NumericSpinner)d;
            decimal min = ctrl.Minimum;
            if ((decimal)value < min)
            {
                return min;
            }
            return value;
        }

        /// <summary>
        ///     Maximum restricts the maximum value of the Value property
        /// </summary>
        public decimal Maximum
        {
            get { return (decimal)GetValue(MaximumProperty); }
            set { SetValue(MaximumProperty, value); }
        }

        /// <summary>
        ///     Called when MaximumProperty is changed on "d."
        /// </summary>
        private static void OnMaximumChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
        {
            d.CoerceValue(ValueProperty);
        }

        /// <summary>
        ///     The DependencyProperty for the Value property.
        ///     Flags:              None
        ///     Default Value:      0
        /// </summary>
        public static readonly DependencyProperty ValueProperty =
            DependencyProperty.Register(
                "Value",
                typeof(decimal),
                typeof(NumericSpinner),
                new FrameworkPropertyMetadata(
                    (decimal)0.0,
                    FrameworkPropertyMetadataOptions.BindsTwoWayByDefault | FrameworkPropertyMetadataOptions.Journal,
                    OnValueChanged,
                    ConstrainToRange));

        private static object ConstrainToRange(DependencyObject d, object value)
        {
            var ctrl = (NumericSpinner)d;
            decimal min = ctrl.Minimum;
            decimal v = (decimal)value;
            if (v < min)
            {
                return min;
            }

            decimal max = ctrl.Maximum;
            if (v > max)
            {
                return max;
            }

            return value;
        }

        /// <summary>
        /// Value property
        /// </summary>
        public decimal Value
        {
            get { return (decimal)GetValue(ValueProperty); }
            set { SetValue(ValueProperty, value); }
        }

        /// <summary>
        ///     Called when Value is changed on "d."
        /// </summary>
        private static void OnValueChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
        {
            var numSpinner = (NumericSpinner)d;

            var routedArgs =
                new RoutedPropertyChangedEventArgs<decimal>((decimal)e.OldValue, (decimal)e.NewValue, ValueChangedEvent);
            numSpinner.OnValueChanged(routedArgs);
        }

        #endregion

        #region Increment

        /// <summary>
        /// The DependencyProperty for the Increment property.
        /// </summary>
        public static readonly DependencyProperty IncrementProperty
            = DependencyProperty.Register("Increment", typeof(decimal), typeof(NumericSpinner),
                                          new FrameworkPropertyMetadata((decimal)1.0),
                                          IsValidIncrement);

        /// <summary>
        /// The increment to increase or decrease the value
        /// </summary>
        public decimal Increment
        {
            get { return (decimal)GetValue(IncrementProperty); }
            set { SetValue(IncrementProperty, value); }
        }

        /// <summary>
        /// Validate input value of Increment
        /// </summary>
        /// <param name="value"></param>
        /// <returns>Returns False if value is NaN or NegativeInfinity or PositiveInfinity or less than or equal zero. Otherwise, returns True.</returns>
        private static bool IsValidIncrement(object value)
        {
            return (decimal)value > 0;
        }

        #endregion Increment

        #region ValueTemplate/Selector

        /// <summary>
        ///     The DependencyProperty for the ValueTemplate property.
        ///     Flags:              None
        ///     Default Value:      null
        /// </summary>
        public static readonly DependencyProperty ValueTemplateProperty =
            DependencyProperty.Register(
                "ValueTemplate",
                typeof(DataTemplate),
                typeof(NumericSpinner),
                new FrameworkPropertyMetadata((DataTemplate)null));


        /// <summary>
        ///     ValueTemplate is the template used to display the value of the control.
        /// </summary>
        public DataTemplate ValueTemplate
        {
            get { return (DataTemplate)GetValue(ValueTemplateProperty); }
            set { SetValue(ValueTemplateProperty, value); }
        }

        /// <summary>
        ///     The DependencyProperty for the ValueTemplateSelector property.
        ///     Flags:              None
        ///     Default Value:      null
        /// </summary>
        public static readonly DependencyProperty ValueTemplateSelectorProperty =
            DependencyProperty.Register(
                "ValueTemplateSelector",
                typeof(DataTemplateSelector),
                typeof(NumericSpinner),
                new FrameworkPropertyMetadata((DataTemplateSelector)null));

        /// <summary>
        ///     ValueTemplateSelector allows the application writer to provide custom logic
        ///     for choosing the template used to display the value of the control.
        /// </summary>
        /// <remarks>
        ///     This property is ignored if <seealso cref="ValueTemplate"/> is set.
        /// </remarks>
        public DataTemplateSelector ValueTemplateSelector
        {
            get { return (DataTemplateSelector)GetValue(ValueTemplateSelectorProperty); }
            set { SetValue(ValueTemplateSelectorProperty, value); }
        }

        #endregion

        #region IsEditable

        /// <summary>
        /// Indicates whether contrl supports editing value by hand or only by buttons.
        /// </summary>
        public bool IsEditable
        {
            get { return (bool)GetValue(IsEditableProperty); }
            set { SetValue(IsEditableProperty, value); }
        }

        public static readonly DependencyProperty IsEditableProperty =
            DependencyProperty.Register("IsEditable", typeof(bool), typeof(NumericSpinner), new UIPropertyMetadata(true));

        #endregion

        #endregion

        #region Public Events

        /// <summary>
        /// An event reporting that the Value property changed.
        /// </summary>
        public event RoutedPropertyChangedEventHandler<decimal> ValueChanged
        {
            add { AddHandler(ValueChangedEvent, value); }
            remove { RemoveHandler(ValueChangedEvent, value); }
        }

        /// <summary>
        /// Event ID correspond to Value changed event
        /// </summary>
        public static readonly RoutedEvent ValueChangedEvent = EventManager.RegisterRoutedEvent("ValueChanged", RoutingStrategy.Bubble, typeof(RoutedPropertyChangedEventHandler<decimal>), typeof(NumericSpinner));

        #endregion

        #region Public Commands

        private static RoutedCommand _increaseCommand = null;
        private static RoutedCommand _decreaseCommand = null;

        /// <summary>
        /// Increase the value property command
        /// </summary>
        public static RoutedCommand IncreaseCommand
        {
            get { return _increaseCommand; }
        }

        /// <summary>
        /// Decrease the value property command
        /// </summary>
        public static RoutedCommand DecreaseCommand
        {
            get { return _decreaseCommand; }
        }

        #endregion

        #region Protected Methods

        /// <summary>
        /// This method is invoked when the Value property changes.
        /// </summary>
        /// <param name="e">RoutedPropertyChangedEventArgs contains the old and new value.</param>
        protected virtual void OnValueChanged(RoutedPropertyChangedEventArgs<decimal> e)
        {
            RaiseEvent(e);
        }

        #endregion

        #region Private Methods

        private static void OnQueryDecreaseCommand(object target, CanExecuteRoutedEventArgs args)
        {
            NumericSpinner spinner = (NumericSpinner)target;
            args.CanExecute = spinner.Value > decimal.MinValue && (spinner.Value - spinner.Increment) >= spinner.Minimum;
        }

        private static void OnExecuteDecreaseCommand(object target, ExecutedRoutedEventArgs args)
        {
            NumericSpinner spinner = (NumericSpinner)target;
            spinner.Value -= spinner.Increment;
        }

        private static void OnQueryIncreaseCommand(object target, CanExecuteRoutedEventArgs args)
        {
            NumericSpinner spinner = (NumericSpinner)target;
            args.CanExecute = spinner.Value < decimal.MaxValue && (spinner.Value + spinner.Increment) <= spinner.Maximum;
        }

        private static void OnExecuteIncreaseCommand(object target, ExecutedRoutedEventArgs args)
        {
            NumericSpinner spinner = (NumericSpinner)target;
            spinner.Value += spinner.Increment;
        }

        private void AttachToVisualTree()
        {
            DetachFromVisualTree();

            _increaseButton = GetTemplateChild("PART_IncreaseButton") as ButtonBase;
            if (_increaseButton != null)
            {
                _increaseButton.IsEnabledChanged += new DependencyPropertyChangedEventHandler(OnIncreaseButtonIsEnabledChanged);
            }

            _decreaseButton = GetTemplateChild("PART_DecreaseButton") as ButtonBase;
            if (_decreaseButton != null)
            {
                _decreaseButton.IsEnabledChanged += new DependencyPropertyChangedEventHandler(OnDecreaseButtonIsEnabledChanged);
            }
        }

        private void DetachFromVisualTree()
        {
            if (_increaseButton != null)
            {
                _increaseButton.IsEnabledChanged -= new DependencyPropertyChangedEventHandler(OnIncreaseButtonIsEnabledChanged);
                _increaseButton = null;
            }

            if (_decreaseButton != null)
            {
                _decreaseButton.IsEnabledChanged -= new DependencyPropertyChangedEventHandler(OnDecreaseButtonIsEnabledChanged);
                _decreaseButton = null;
            }
        }

        private void OnDecreaseButtonIsEnabledChanged(object sender, DependencyPropertyChangedEventArgs e)
        {
            if (!(bool)e.NewValue)
            {
                SwitchFocus(_increaseButton);
            }
        }

        private void OnIncreaseButtonIsEnabledChanged(object sender, DependencyPropertyChangedEventArgs e)
        {
            if (!(bool)e.NewValue)
            {
                SwitchFocus(_decreaseButton);
            }
        }

        /// <summary>
        /// Defer the move focus action to ensure the focus will move to the input button correctly
        /// </summary>
        private void SwitchFocus(ButtonBase button)
        {
            Dispatcher.BeginInvoke(DispatcherPriority.ApplicationIdle,
                                   new DispatcherOperationCallback(delegate(object param)
                                                                       {
                                                                           Keyboard.Focus((IInputElement)param);
                                                                           return null;
                                                                       }), button);
        }

        #endregion

        private ButtonBase _increaseButton, _decreaseButton;
    }

    internal class NumericSpinnerValueConverter : IValueConverter
    {
        public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
        {
            var v = (decimal)value;

            return v.ToString();
        }

        public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
        {
            var v = (string) value;
            decimal cv;
            decimal.TryParse(v, out cv);

            return decimal.TryParse(v, out cv) ? cv : 0;
        }
    }
}